﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace CodeAnalysisRuleCollection.Common.CSharp
{
    /// <example>@_wrongName</example>
    [ExportCodeFixProvider(TypeNameDoesNotMeetNamingGuidelinesDiagnosticAnalyzer.DiagnosticId, LanguageNames.CSharp)]
    internal class TypeNameDoesNotMeetNamingGuidelinesCodeFixProvider : ICodeFixProvider
    {
        public IEnumerable<string> GetFixableDiagnosticIds()
        {
            return new[] { TypeNameDoesNotMeetNamingGuidelinesDiagnosticAnalyzer.DiagnosticId };
        }

        public async Task<IEnumerable<CodeAction>> GetFixesAsync(Document document, TextSpan span, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            var diagnosticSpan = diagnostics.First().Location.SourceSpan;

            // Find the type declaration identified by the diagnostic.
            var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<TypeDeclarationSyntax>().First();

            string originalName = declaration.Identifier.ValueText;

            // If can not fix
            if (!originalName.Any(Char.IsLetter))
                return null;

            string newName = CorrectName(originalName);

            var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
            if (semanticModel.LookupSymbols(declaration.Identifier.Span.Start, name: newName).Any())
                return null;

            // Return a code action that will invoke the fix.
            return new[] { CodeAction.Create("Fix type name", c => FixNameAsync(document, declaration, semanticModel, c)) };
        }

        private async Task<Solution> FixNameAsync(Document document, TypeDeclarationSyntax typeDecl, SemanticModel semanticModel, CancellationToken cancellationToken)
        {
            // Compute new uppercase name.
            var identifierToken = typeDecl.Identifier;
            var newName = CorrectName(identifierToken.ValueText);

            // Get the symbol representing the type to be renamed.
            var typeSymbol = semanticModel.GetDeclaredSymbol(typeDecl, cancellationToken);

            // Produce a new solution that has all references to that type renamed, including the declaration.
            var originalSolution = document.Project.Solution;
            var optionSet = originalSolution.Workspace.GetOptions();
            var newSolution = await Renamer.RenameSymbolAsync(document.Project.Solution, typeSymbol, newName, optionSet, cancellationToken).ConfigureAwait(false);

            // Return the new solution with the now-uppercase type name.
            return newSolution;
        }

        protected static string CorrectName(string name)
        {
            string newName = name.Replace("_", String.Empty);

            while ((int underscoreIndex = newName.IndexOf('_')) != -1)
            {
                newName = newName.Remove(underscoreIndex, 1);

                if (underscoreIndex < newName.Length && Char.IsLower(newName[underscoreIndex]))
                {
                    char @char = newName[underscoreIndex];
                    newName = newName.Remove(underscoreIndex, 1).Insert(underscoreIndex, Char.ToUpperInvariant(@char).ToString());
                }
            }

            newName = String.Concat(Char.ToUpperInvariant(newName[0]), newName.Substring(1));

            return newName;
        }
    }

    [DiagnosticAnalyzer]
    [ExportDiagnosticAnalyzer(DiagnosticId, LanguageNames.CSharp)]
    public class TypeNameDoesNotMeetNamingGuidelinesDiagnosticAnalyzer : ISymbolAnalyzer
    {
        internal const string DiagnosticId = "TypeNameDoesNotMeetNamingGuidelines";
        internal const string Description = "Type name does not meet naming guidelines";
        internal const string MessageFormat = "Type name '{0}' does not meet naming guidelines";
        internal const string Category = "Naming";

        private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Description, MessageFormat, Category, DiagnosticSeverity.Warning);

        public ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }

        public ImmutableArray<SymbolKind> SymbolKindsOfInterest { get { return ImmutableArray.Create(SymbolKind.NamedType); } }

        public void AnalyzeSymbol(ISymbol symbol, Compilation compilation, Action<Diagnostic> addDiagnostic, CancellationToken cancellationToken)
        {
            var namedTypeSymbol = symbol as INamedTypeSymbol;
            string name = namedTypeSymbol.Name;

            // do not analyze if it is an interop type
            if (
                (var structLayoutSymbol = compilation.GetTypeByMetadataName(typeof(StructLayoutAttribute).FullName)) != null &&
                namedTypeSymbol.GetAttributes().Any(a => a.AttributeClass.Equals(structLayoutSymbol))
            )
                return;

            if (
                (var compilerGeneratedSymbol = compilation.GetTypeByMetadataName(typeof(CompilerGeneratedAttribute).FullName)) != null &&
                namedTypeSymbol.GetAttributes().Any(a => a.AttributeClass.Equals(compilerGeneratedSymbol))
            )
                return;

            // Find just those named type symbols with incorrect names.
            if (Char.IsLower(name[0]) || name.Contains('_') || name[0] == '@')
                addDiagnostic(Diagnostic.Create(Rule, namedTypeSymbol.Locations[0], name));
        }
    }
}
